LEÇON 9

Programmation Asynchrone en Python

async/await, asyncio, coroutines, et concurrence

Français - Programmation Asynchrone

Qu'est-ce que l'asynchrone ? La programmation asynchrone permet d'exécuter plusieurs tâches en attente (I/O, requêtes réseau, etc.) simultanément sans bloquer le programme, contrairement au code synchrone classique.

1. Synchrone vs Asynchrone

SynchroneAsynchrone
Les tâches s'exécutent une par unePlusieurs tâches peuvent être en attente
Une tâche lente bloque tout le programmePendant l'attente, d'autres tâches s'exécutent
Plus simple à comprendre et déboguerPlus efficace pour les opérations I/O
Moins efficace pour beaucoup d'opérations lentesIdéal pour API, bases de données, fichiers
# Exemple synchrone (bloquant)
import time

def tache_lente(nom):
    print(f"Début de {nom}")
    time.sleep(2) # Simulation d'opération lente
    print(f"Fin de {nom}")
    return f"Résultat de {nom}"

debut = time.time()
resultat1 = tache_lente("Tâche 1")
resultat2 = tache_lente("Tâche 2")
print(f"Temps total: {time.time() - debut} secondes")
# Prend ~4 secondes (2 + 2)
# Exemple asynchrone (non-bloquant) avec asyncio
import asyncio

async def tache_lente_async(nom):
    print(f"Début de {nom}")
    await asyncio.sleep(2) # Non-bloquant
    print(f"Fin de {nom}")
    return f"Résultat de {nom}"

async def main():
    debut = time.time()
    # Exécution concurrente
    resultats = await asyncio.gather(
        tache_lente_async("Tâche 1"),
        tache_lente_async("Tâche 2")
    )
    print(f"Temps total: {time.time() - debut} secondes")
    print(resultats)

asyncio.run(main())
# Prend ~2 secondes (exécution parallèle)

2. Mots-clés async/await

# Définir une fonction asynchrone (coroutine)
async def ma_coroutine():
    return "Résultat"

# Appeler une coroutine (ne s'exécute pas immédiatement)
coro = ma_coroutine()
#

# Exécuter une coroutine avec await
async def utilisateur():
    resultat = await ma_coroutine()
    print(resultat)

# Point d'entrée principal
asyncio.run(utilisateur())
Règles importantes :
  • Une fonction async doit être appelée avec await ou asyncio.run()
  • await ne peut être utilisé que dans une fonction async
  • Les fonctions async retournent un objet coroutine, pas la valeur directement
  • Ne pas oublier await avant les opérations asynchrones (sinon la coroutine ne s'exécute pas)

3. Exécution concurrente avec asyncio

import asyncio

async def tache(numero, duree):
    print(f"Tâche {numero} démarre (durée {duree}s)")
    await asyncio.sleep(duree)
    print(f"Tâche {numero} terminée")
    return f"Résultat {numero}"

async def exemples_concurrents():
    # Méthode 1: asyncio.gather() - exécute plusieurs coroutines
    resultats = await asyncio.gather(
        tache(1, 2),
        tache(2, 1),
        tache(3, 3)
    )
    print("Résultats gather:", resultats)

# Méthode 2: asyncio.create_task() - crée des tâches individuellement
tache1 = asyncio.create_task(tache(4, 2))
tache2 = asyncio.create_task(tache(5, 1))
await tache1
await tache2

# Méthode 3: asyncio.wait() - attend la première ou toutes les tâches
taches = [tache(6, 2), tache(7, 1)]
termine, en_attente = await asyncio.wait(taches, return_when=asyncio.FIRST_COMPLETED)
print("Première tâche terminée")

4. Timeouts et délais

import asyncio

async def operation_lente():
    print("Début opération lente...")
    await asyncio.sleep(5)
    return "Opération réussie"

async def avec_timeout():
    try:
        # Timeout après 2 secondes
        resultat = await asyncio.wait_for(operation_lente(), timeout=2)
        print(resultat)
    except asyncio.TimeoutError:
        print("L'opération a pris trop de temps!")

async def avec_timeout_partiel():
    # Timeout sur une partie seulement
    try:
        async with asyncio.timeout(2): # Python 3.11+
            await asyncio.sleep(1)
            print("Cette partie s'exécute")
            await asyncio.sleep(2) # Dépassement du timeout
    except TimeoutError:
        print("Timeout déclenché")

5. Synchronisation entre tâches

import asyncio

# Verrou (Lock) - empêche l'accès concurrent
verrou = asyncio.Lock()
compteur_partage = 0

async def incrementer_avec_verrou():
    global compteur_partage
    async with verrou:
        valeur = compteur_partage
        await asyncio.sleep(0.1)
        compteur_partage = valeur + 1

# Événement (Event) - signale entre tâches
evenement = asyncio.Event()

async def attendre_signal():
    print("En attente du signal...")
    await evenement.wait()
    print("Signal reçu!")

async def envoyer_signal():
    await asyncio.sleep(2)
    evenement.set()
    print("Signal envoyé")

# File d'attente (Queue) - partage de données
queue = asyncio.Queue()

async def producteur():
    for i in range(5):
        await queue.put(f"Donnée {i}")
        await asyncio.sleep(0.5)

async def consommateur():
    while True:
        donnee = await queue.get()
        print(f"Traitement: {donnee}")

6. Applications pratiques

# Requêtes HTTP asynchrones (aiohttp)
# pip install aiohttp
import aiohttp
import asyncio

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def fetch_multiple():
    urls = [
        "https://api.github.com",
        "https://httpbin.org/get",
        "https://jsonplaceholder.typicode.com/posts/1"
    ]
    async with aiohttp.ClientSession() as session:
        taches = [fetch_url(session, url) for url in urls]
        resultats = await asyncio.gather(*taches)
        for url, contenu in zip(urls, resultats):
            print(f"{url}: {len(contenu)} caractères")
# Lecture de fichiers asynchrone (aiofiles)
# pip install aiofiles
import aiofiles
import asyncio

async def lire_fichier_async(chemin):
    async with aiofiles.open(chemin, mode='r') as fichier:
        contenu = await fichier.read()
        return contenu

async def lire_plusieurs(fichiers):
    taches = [lire_fichier_async(f) for f in fichiers]
    resultats = await asyncio.gather(*taches)
    return resultats
Bonnes pratiques :
  • ✔️ Utilisez asyncio.run() comme point d'entrée unique
  • ✔️ Préférez asyncio.gather() pour exécuter plusieurs coroutines
  • ✔️ Évitez les opérations bloquantes (time.sleep, requests.get) dans le code async
  • ✔️ Utilisez asyncio.to_thread() pour les opérations CPU-intensives
  • ✔️ Ne mélangez pas code synchrone et asynchrone dans la même fonction
  • ✔️ Testez avec pytest-asyncio
Attention : La programmation asynchrone n'accélère pas les opérations CPU-intensives. Pour cela, utilisez le multiprocessing ou concurrent.futures. L'asynchrone est idéal pour les I/O (réseau, disque, bases de données).

English - Asynchronous Programming

What is asynchronous programming? Asynchronous programming allows multiple waiting tasks (I/O, network requests, etc.) to run simultaneously without blocking the program, unlike traditional synchronous code.

1. Synchronous vs Asynchronous

SynchronousAsynchronous
Tasks execute one after anotherMultiple tasks can be pending
A slow task blocks the whole programOther tasks run while waiting
Easier to understand and debugMore efficient for I/O operations
Less efficient for many slow operationsIdeal for APIs, databases, files
# Synchronous example (blocking)
import time

def slow_task(name):
    print(f"Start {name}")
    time.sleep(2) # Simulate slow operation
    print(f"End {name}")
    return f"Result of {name}"

start = time.time()
result1 = slow_task("Task 1")
result2 = slow_task("Task 2")
print(f"Total time: {time.time() - start} seconds")
# Takes ~4 seconds (2 + 2)

2. async/await Keywords

# Define an asynchronous function (coroutine)
async def my_coroutine():
    return "Result"

# Call a coroutine (doesn't execute immediately)
coro = my_coroutine()
#

# Execute a coroutine with await
async def user():
    result = await my_coroutine()
    print(result)

# Main entry point
asyncio.run(user())

3. Concurrent Execution with asyncio

import asyncio

async def task(number, duration):
    print(f"Task {number} starts (duration {duration}s)")
    await asyncio.sleep(duration)
    print(f"Task {number} finished")
    return f"Result {number}"

async def concurrent_examples():
    # Method 1: asyncio.gather() - runs multiple coroutines
    results = await asyncio.gather(
        task(1, 2),
        task(2, 1),
        task(3, 3)
    )
    print("Gather results:", results)

# Method 2: asyncio.create_task() - creates individual tasks
task1 = asyncio.create_task(task(4, 2))
task2 = asyncio.create_task(task(5, 1))
await task1
await task2

4. Timeouts and Delays

import asyncio

async def slow_operation():
    print("Starting slow operation...")
    await asyncio.sleep(5)
    return "Operation successful"

async def with_timeout():
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=2)
        print(result)
    except asyncio.TimeoutError:
        print("Operation took too long!")

5. Task Synchronization

import asyncio

# Lock - prevents concurrent access
lock = asyncio.Lock()
shared_counter = 0

async def increment_with_lock():
    global shared_counter
    async with lock:
        value = shared_counter
        await asyncio.sleep(0.1)
        shared_counter = value + 1

# Event - signals between tasks
event = asyncio.Event()

async def wait_for_signal():
    print("Waiting for signal...")
    await event.wait()
    print("Signal received!")

# Queue - data sharing
queue = asyncio.Queue()

6. Practical Applications

# Async HTTP requests (aiohttp)
# pip install aiohttp
import aiohttp
import asyncio

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def fetch_multiple():
    urls = [
        "https://api.github.com",
        "https://httpbin.org/get"
    ]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        return results
Best practices:
  • ✔️ Use asyncio.run() as the single entry point
  • ✔️ Prefer asyncio.gather() to run multiple coroutines
  • ✔️ Avoid blocking operations (time.sleep, requests.get) in async code
  • ✔️ Use asyncio.to_thread() for CPU-intensive operations
  • ✔️ Don't mix synchronous and asynchronous code in the same function
Warning: Asynchronous programming does NOT speed up CPU-intensive operations. For that, use multiprocessing or concurrent.futures. Async is ideal for I/O (network, disk, databases).